package com.itclass.auth.authentication.provider.support;

import com.itclass.auth.authentication.provider.AbstractUserDetailsAuthenticationProvider;
import com.itclass.auth.authentication.token.supprt.LdapAuthenticationToken;
import com.itclass.auth.authentication.token.supprt.UsernamePasswordLoginAuthenticationToken;
import com.itclass.auth.pojo.dto.AuthUserDetails;
import com.itclass.auth.pojo.dto.UserDetailsDTO;
import com.itclass.auth.service.SysUserService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.authentication.LdapAuthenticator;
import org.springframework.security.ldap.ppolicy.PasswordPolicyException;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;


public class LdapAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider<LdapAuthenticationToken> {
    private LdapAuthenticator authenticator;

    private LdapAuthoritiesPopulator authoritiesPopulator;

    @Autowired
    private SysUserService sysUserService;

    public LdapAuthenticationProvider(LdapAuthenticator authenticator, LdapAuthoritiesPopulator authoritiesPopulator) {
        this.authenticator = authenticator;
        this.authoritiesPopulator = authoritiesPopulator;
    }

    private boolean hideUserNotFoundExceptions = true;
    protected final Log logger = LogFactory.getLog(getClass());

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    private boolean useAuthenticationRequestCredentials = true;

    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    protected UserDetailsContextMapper userDetailsContextMapper = new LdapUserDetailsMapper();

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, LdapAuthenticationToken authentication) throws AuthenticationException {

    }

    @Override
    protected void check(Authentication authentication) {
        Assert.isInstanceOf(LdapAuthenticationToken.class, authentication, () -> this.messages.getMessage("LdapAuthenticationProvider.onlySupports", "Only LdapAuthenticationToken is supported"));
    }

    @Override
    protected UserDetails retrieveUser(String username, Authentication authentication) throws AuthenticationException {

        String password = (String) authentication.getCredentials();
        if (!StringUtils.hasLength(username)) {
            throw new BadCredentialsException(this.messages.getMessage("LdapAuthenticationProvider.emptyUsername", "Empty Username"));
        }
        if (!StringUtils.hasLength(password)) {
            throw new BadCredentialsException(this.messages.getMessage("AbstractLdapAuthenticationProvider.emptyPassword", "Empty Password"));
        }
        Assert.notNull(password, "Null password was supplied in authentication token");

        UsernamePasswordAuthenticationToken unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(authentication.getPrincipal(), authentication.getCredentials());
        DirContextOperations userData = doAuthentication(unauthenticated);
        UserDetails user = this.userDetailsContextMapper.mapUserFromContext(userData, authentication.getName(),
                loadUserAuthorities(userData, authentication.getName(), (String) authentication.getCredentials()));
        // 保存ldap 用户数据一份到数据库
        try {
            sysUserService.saveLdapUser(user,userData);
        } catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
        return user;
    }

    private Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username, String password) {
        return authoritiesPopulator.getGrantedAuthorities(userData, username);
    }

    private DirContextOperations doAuthentication(Authentication authentication) {
        try {
            return authenticator.authenticate(authentication);
        } catch (PasswordPolicyException ex) {
            // The only reason a ppolicy exception can occur during a bind is that the
            // account is locked.
            throw new LockedException(this.messages.getMessage(ex.getStatus().getErrorCode(), ex.getStatus().getDefaultMessage()));
        } catch (UsernameNotFoundException ex) {
            if (this.hideUserNotFoundExceptions) {
                throw new BadCredentialsException(this.messages.getMessage("LdapAuthenticationProvider.badCredentials", "Bad credentials"));
            }
            throw ex;
        } catch (NamingException ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (LdapAuthenticationToken.class.isAssignableFrom(authentication));
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {

        LdapUserDetailsImpl ldapUserDetails = (LdapUserDetailsImpl) user;
        LdapAuthenticationToken result = new LdapAuthenticationToken();
        List<String> authorities = ldapUserDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        result.setPrincipal(ldapUserDetails.getUsername());
        result.setAuthorities(ldapUserDetails.getAuthorities());
        UserDetailsDTO userDetailsDTO = new UserDetailsDTO();
        userDetailsDTO.setName(ldapUserDetails.getUsername());
        userDetailsDTO.setUsername(ldapUserDetails.getUsername());
        userDetailsDTO.setRoles(authorities);
        result.setDetails(userDetailsDTO);
        return result;
    }
}
